﻿/* ****************************************************************************
 *
 * Copyright (c) Francesco Abbruzzese. All rights reserved.
 * francesco@dotnet-programming.com
 * http://www.dotnet-programming.com/
 * 
 * This software is subject to a dual license model
 * and included in the license.txt file of this distribution.
 * 
 * You must not remove this notice, or any other, from this software.
 *
 * ***************************************************************************/
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.Generic;
using System.Reflection;
using System.ComponentModel.DataAnnotations;
using System.Windows.Data;
using System.Globalization;
using System.Text;
using System.Dynamic;
using System.Collections;
using System.Collections.ObjectModel;


namespace BindToolkit
{
    public partial class BindWrapper: DynamicObject, INotifyDataErrorInfo, INotifyPropertyChanged
    {


        protected object WrappedObject { get; set; }
        protected Type WrappedType { get; set; }
        protected Dictionary<string, List<string>> errors;
        protected Dictionary<string, List<string>> simpleErrors;
        protected Dictionary<string, BindWrapper> wrappers;
        protected List<BindWrapper> sonsInError;
        protected Dictionary<string, object> collections;
        public object this[string x]
        {
            get
            {
                
                if (wrappers != null && wrappers.ContainsKey(x)) return wrappers[x];
                if (collections != null && collections.ContainsKey(x)) return collections[x];
                PropertyInfo pi = WrappedType.GetProperty(x);
                if (pi == null) return null;
                return pi.GetValue(WrappedObject, new object[0]);
            }
            set
            {
                PropertyInfo pi = WrappedType.GetProperty(x);
                bool changed = false;
                if (value != null)
                {
                    if (value.GetType() == typeof(BindWrapper))
                    {
                        if (wrappers == null) wrappers = new Dictionary<string, BindWrapper>();
                        wrappers[x] = value as BindWrapper;
                        value = (value as BindWrapper).WrappedObject;
                        changed= true;
                    }
                    if (value.GetType().GetInterface("System.Collections.IEnumerable", false) != null && value.GetType() != typeof(string) && (pi == null || !pi.PropertyType.IsAssignableFrom(value.GetType())))
                    {
                        AddToCollections(x, value);
                    }
                }
                else
                {
                    if (wrappers != null && wrappers.ContainsKey(x))
                    {
                        wrappers.Remove(x);
                        changed = true; ;
                    }
                }
                if (pi != null)
                {
                    if (value != null && ! pi.PropertyType.IsAssignableFrom(value.GetType()) )
                    {
                        IConvertible conv = value as IConvertible;
                        if (conv != null)
                        {
                            object prev = pi.GetValue(WrappedObject, new object[0]);
                            object newest = null;
                            try
                            {
                                
                                newest = conv.ToType(pi.PropertyType, CultureInfo.CurrentUICulture);
                            }
                            catch(Exception e)
                            {
                                string error = getFormatError(pi, value);
                                if (value == null || (value as string) == string.Empty || value == DependencyProperty.UnsetValue)
                                {
                                    if (!pi.PropertyType.IsValueType)
                                    {
                                        newest=null;
                                    }
                                    else
                                    {
                                        newest = Activator.CreateInstance(pi.PropertyType);
                                    }
                                }
                                else if (error != null)
                                {
                                    throw (new FormatException(error));
                                    
                                }
                                else if (value.GetType() == typeof(string))
                                    throw (e);
                                else
                                    throw new InvalidCastException(string.Format(Resource.UnableToConvert, value.GetType().ToString(), pi.PropertyType.ToString()));
                            }
                            if (Validate(x, newest))
                            {
                                
                               
                            }
                            pi.SetValue(WrappedObject, newest, new object[0]);
                            changed = prev != newest;
                            
                        }
                        else
                            throw new InvalidCastException(string.Format(Resource.UnableToConvert, value.GetType().ToString(), pi.PropertyType.ToString()));
                    }
                    else
                    {
                        object prev = pi.GetValue(WrappedObject, new object[0]);
                        if (Validate(x, value))
                        {
                           
                        }
                        changed = prev != value;
                        pi.SetValue(WrappedObject, value, new object[0]);

                    }
                }
                if (PropertyChanged != null && changed)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(string.Format("Item[{0}]", x)));
                    
                }
                if (changed) triggerDependencies(x);
            }
        }

        public BindWrapper(object wrappedObject, bool wrapDeep = false, bool wrapEnumerables=false)
            : base()
        {
            if (wrappedObject == null) throw new ArgumentNullException("wrappedObject");
            WrappedObject = wrappedObject;
            WrappedType = wrappedObject.GetType();
            errors = new Dictionary<string, List<string>>();
            simpleErrors = new Dictionary<string, List<string>>();
            if (wrapDeep) recursiveWrapping(wrapEnumerables);
        }
        public BindWrapper(Type wrappedType)
            : base()
        {
            if (wrappedType == null) throw new ArgumentNullException("wrappedType");
            ConstructorInfo ci = wrappedType.GetConstructor(new Type[0]);
            if (ci == null) throw new ArgumentException(string.Format(Resource.No0ArgumentsConstructor, wrappedType), "wrappedType");
            WrappedObject = wrappedType.GetConstructor(new Type[0]).Invoke(new object[0]);
            WrappedType = wrappedType;
            errors = new Dictionary<string, List<string>>();
            simpleErrors = new Dictionary<string, List<string>>();
        }
        

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {

            try
            {
                result = this[binder.Name];
            }
            catch
            {
                result = null;
                return false;
            }
            return true;
        }
        public bool TryGetMember(string name, out object result)
        {

            try
            {
                result = this[name];
            }
            catch
            {
                result = null;
                return false;
            }
            return true;
        }
         
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if (binder.ReturnType == WrappedType)
            {
                result = WrappedObject;
                return true;
            }
            result = null;
            return false;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            try
            {
                this[binder.Name] = value;
            }
            catch
            {
                return false;
            }
            return true;

        }
        
        protected void AddErrors(string field, List<ValidationResult> errors)
        {
           
            foreach (ValidationResult vr in errors)
            {
                if (string.IsNullOrEmpty(vr.ErrorMessage)) continue;
                if (field != null)
                {
                    AddSimpleError(field, vr.ErrorMessage);
                    continue;
                }
                if (vr.MemberNames == null || !vr.MemberNames.GetEnumerator().MoveNext())
                    AddError(string.Empty, vr.ErrorMessage);
                else
                    foreach (string se in vr.MemberNames)
                        AddError(se, vr.ErrorMessage);

            }
            
        }
        protected bool Validate(string field, object val)
        {
           
            ValidationContext ctx = new ValidationContext(WrappedObject, null, null);
            ctx.MemberName = field;
            List<ValidationResult> errors = new List<ValidationResult>();
            Validator.TryValidateProperty(val, ctx, errors);
            bool success = true;
            foreach (ValidationResult res in errors)
            {
                if (res != ValidationResult.Success && (!string.IsNullOrEmpty(res.ErrorMessage))) success = false;
            }
            handlesValidationDependencies(field, errors);
            RemoveAllErrors(field);
            if (!success) AddErrors(field, errors);
            
            return success;
        }

        protected void AddError(string propertyName, string error)
        {
            propertyName = propagateDown(propertyName, error);
            if (propertyName == null) return;
            if (propertyName.Length != 0 && WrappedType.GetProperty(propertyName) == null
                && (wrappers == null || !wrappers.ContainsKey(propertyName))) propertyName = string.Empty;
            
            if (!errors.ContainsKey(propertyName))
                errors[propertyName] = new List<string>();

            if (!errors[propertyName].Contains(error))
            {
                errors[propertyName].Add(error);
                
            }
            if (ErrorsChanged != null)
            {
                if (propertyName==string.Empty)
                    ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Empty));
                else
                    ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Format("Item[{0}]", propertyName)));
            }
        }
        protected void AddSimpleError(string propertyName, string error)
        {
            propertyName = propagateDown(propertyName, error);
            if (propertyName == null) return;
            if (propertyName.Length != 0 && WrappedType.GetProperty(propertyName) == null
                && (wrappers == null || !wrappers.ContainsKey(propertyName))) propertyName = string.Empty;

            if (!simpleErrors.ContainsKey(propertyName))
                simpleErrors[propertyName] = new List<string>();

            if (!simpleErrors[propertyName].Contains(error))
            {
                simpleErrors[propertyName].Add(error);
                
            }
            if (ErrorsChanged != null)
            {
                if (propertyName == string.Empty)
                    ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Empty));
                else
                    ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Format("Item[{0}]", propertyName)));
            };
        }
        protected void NewSonInError(BindWrapper bw)
        {
            if (sonsInError == null) sonsInError = new List<BindWrapper>();
            sonsInError.Add(bw);
        }
        private string propagateDown(string propertyName, string error)
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return string.Empty;
            int pos = propertyName.IndexOf('.');
            int pos1 = propertyName.IndexOf('[');
            string field = null;
            string tail = null;
            if (pos < 0)
            {
                if (pos1 < 0)
                {
                    field = propertyName;
                }
                else
                {
                    field = propertyName.Substring(0, pos1);
                    tail = propertyName.Substring(pos1);
                }
            }
            else
            {
                if (pos1 < 0)
                {
                    field = propertyName.Substring(0, pos);
                    if (propertyName.Length > pos + 1)
                        tail = propertyName.Substring(pos + 1);
                }
                else
                {
                    if (pos < pos1)
                    {
                        field = propertyName.Substring(0, pos);
                        if (propertyName.Length > pos + 1)
                            tail = propertyName.Substring(pos + 1);
                    }
                    else
                    {
                        field = propertyName.Substring(0, pos1);
                        tail = propertyName.Substring(pos1);
                    }
                }
            }
           
            PropertyInfo pi = WrappedType.GetProperty(field);

            object propertyValue = null;
            try
            {
                propertyValue = this[field];
            }
            catch
            {
                return string.Empty;
            }
            if (propertyValue != null && tail != null && tail.Length > 0)
            {
                if (propertyValue.GetType() == typeof(BindWrapper))
                {
                    
                    (propertyValue as BindWrapper).AddError(tail, error);
                     NewSonInError(propertyValue as BindWrapper);
                     return null;
                }
                else
                {
                    IEnumerable en = propertyValue as IEnumerable;
                    if (en == null || propertyValue is string) return field;
                    else
                    {
                        if (!propagateDownIndices(en, tail, error)) return field;
                        else return null;
                    }
                }
            }
            else
            {
                return field;
            }
            
        }
        private bool TryExtractItem(IEnumerable en, string key, ref object result)
        {
            result=null;
            Type t = en.GetType();
            //try integer indexer
            PropertyInfo pi = null;
            try { pi = t.GetProperty("Item"); } catch { };
            if (pi == null) return false;

            long longIndex = 0;
            bool isLong = long.TryParse(key, out longIndex);
            int intIndex = 0;
            bool isInt = int.TryParse(key, out intIndex);

            if (isInt)
            {
                try { result = pi.GetValue(en, new object[] { intIndex }); } 
                catch { }
                if (result != null) return true;
            }
            else if (isLong)
            {
                try { result = pi.GetValue(en, new object[] { intIndex }); }
                catch { }
                if (result != null) return true;
            }
            else
            {
                try { result = pi.GetValue(en, new object[] { key }); }
                catch { }
                if (result != null) return true;
            }
            return false;
        }
        private bool propagateDownIndices(IEnumerable en, string tail, string error)
        {
            if (tail.Length == 0) return false;
            if (tail[0] != '[') return false;
            int pos = tail.IndexOf(']');
            if (pos<0) return false;
            string index =tail.Substring(1, pos-1).Trim();
            
            
            if (tail.Length>pos+1) tail=tail.Substring(pos+1);
            else tail=string.Empty;
            object nextLevel=null;

            if (!TryExtractItem(en, index, ref nextLevel)) return false;

            if(nextLevel == null) return false;
            if (nextLevel.GetType() == typeof(BindWrapper))
            {
                (nextLevel as BindWrapper).AddError(tail.Substring(1), error);
                NewSonInError(nextLevel as BindWrapper);
                return true;
            }
            else
            {
                en = nextLevel as IEnumerable;
                if (en == null || nextLevel is string) return false;
                else return propagateDownIndices(en, tail.Trim(), error);
            }
        }
       
        
        
        public void RemoveAllErrors(string propertyName)
        {
            propertyName = propagateDown(propertyName, null);
            if (simpleErrors.ContainsKey(propertyName))
            {
                simpleErrors.Remove(propertyName);
                if (ErrorsChanged != null)
                {
                    if (propertyName == string.Empty)
                        ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Empty));
                    else
                        ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Format("Item[{0}]", propertyName)));
                }
            }
        }
        
        
        protected void ResetAndInitializeComplexerrors()
        {
            
            if (sonsInError != null)
                foreach (BindWrapper bw in sonsInError) bw.ResetAndInitializeComplexerrors();
            sonsInError = null;
            if (errors == null) return;
            Dictionary<string, List<string>> toDestroy = errors;
            errors = new Dictionary<string, List<string>>();
            if (toDestroy != null)
            {
                foreach (string key in toDestroy.Keys)
                {
                    if (ErrorsChanged != null)
                    {
                        if (key == string.Empty)
                            ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Empty));
                        else
                            ErrorsChanged(this, new DataErrorsChangedEventArgs(string.Format("Item[{0}]", key)));
                    }
                }
            }
            
        }
        public bool ValidateWholeObject(IEnumerable<ValidationException> remoteErrors=null)
        {
            ResetAndInitializeComplexerrors();
            bool success = ValidateWholeObjectRec(null);
            success = success && AddRemoteErrors(remoteErrors);
            return success;
 
        }
        public bool AddRemoteErrors(IEnumerable<ValidationException> remoteErrors)
        {
            bool success = true;
            if (remoteErrors != null)
            {
                foreach (ValidationException remoteError in remoteErrors)
                {
                    if (remoteError != null && remoteError.ValidationResult != null && !string.IsNullOrEmpty(remoteError.ValidationResult.ErrorMessage))
                    {
                        success = false;
                        if (remoteError.ValidationResult.MemberNames == null)
                            AddError(string.Empty, remoteError.ValidationResult.ErrorMessage);
                        else
                            foreach (string se in remoteError.ValidationResult.MemberNames)
                                AddError(se, remoteError.ValidationResult.ErrorMessage);
                    }
                }
            }
            return success;
        }
        public void ResetGlobalEvaluation()
        {
            ResetAndInitializeComplexerrors();
        }
        protected bool ValidateWholeObjectRec(object father, string columnName=null)
        {
            bool success=true;
            if (wrappers != null)
            {
                foreach (string key in wrappers.Keys)
                {
                    BindWrapper bw = wrappers[key];
                    if (bw != null && !bw.ValidateWholeObjectRec(WrappedObject, key)) success = false;
                }
            }
             
            List<ValidationResult> errorsList = new List<ValidationResult>();
            bool partialSuccess = Validator.TryValidateObject(WrappedObject, new ValidationContext(WrappedObject, null, null), errorsList);
            if (!partialSuccess) AddErrors(null, errorsList);
            success = success && partialSuccess;
            if (father != null)
            {
                ValidationContext ctx = new ValidationContext(father, null, null);
                ctx.MemberName = columnName;
                Validator.TryValidateProperty(WrappedObject, ctx, errorsList);
                partialSuccess = true;
                foreach (ValidationResult res in errorsList)
                {
                    if (res != ValidationResult.Success && (!string.IsNullOrEmpty(res.ErrorMessage))) success = false;
                }
                handlesValidationDependencies(columnName, errorsList);
                if (!partialSuccess) AddErrors(null, errorsList);
                success = success && partialSuccess;
            }
            
            return success;
        }
        private void controlPropertyErrors(string property)
        {
            Validate(property, this[property]);
        }
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        protected List<string> Concat(List<string> x, List<string> y)
        {
            List<string> res = new List<string>();
            foreach(string s in x) res.Add(s);
            foreach(string s in y) res.Add(s);
            return res;
        }
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (propertyName == null || simpleErrors== null) return null;
            propertyName = propertyName.Replace("Item[", string.Empty).Replace("]", string.Empty);
            if (simpleErrors.ContainsKey(propertyName))
            {
                if (errors.ContainsKey(propertyName)) return Concat(simpleErrors[propertyName], errors[propertyName]);
                else return simpleErrors[propertyName];
            }
            else if (errors.ContainsKey(propertyName))
            {
                return errors[propertyName];
            }
            else return null;


        }

        public bool HasErrors
        {
            get
            {
                bool sonHaveErrors = false;
                if (wrappers != null)
                {
                    foreach (string key in wrappers.Keys)
                    {
                        BindWrapper bw = wrappers[key];
                        if (bw != null && !bw.HasErrors) sonHaveErrors = true;
                    }
                }

                return (errors != null && errors.Count > 0) || (simpleErrors != null && simpleErrors.Count > 0) || sonHaveErrors;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}
